/**
 * 计算简单的数学计算
 * 主要处理浮点数问题与科学计算显示，由过大或过小显示的数值处理
 * 默认处理小数点后14位数，处理整数值时防止数值过大使用BigInt类型处理数值计算
 * 为防止出现浮点数和科学计算值在计算过程或者得到的结果，基本上都是在字符串和BigInt类型下计算和处理
 * 在传入计算值中，可以使用数值的string或者数值number传入，
 * 在此后会使用科学计算方法处理得到数值的string
 * 因此当数值过大请输入字符串，否在数值有可能自动转成科学计算值，这样获得的最终值有可能有差别
 * 此插件可以设置数值的小数点后几位数
 */

// 该函数获取的是小数点有多少位数，用来把浮点数转为整数
function beiShuFn(str: string): string {
    let text = '1'
    if (str) text = text.padEnd(str.length + 1, '0')
    return text
}

export default class NumberFormat {
    private digit: number
    private maxDigit: bigint
    // digit最高小数点输入（默认14位）
    constructor(digit: number = 14) {
        const dian = 1
        this.digit = digit
        this.maxDigit = BigInt(dian.toFixed(this.digit).replace('.', ''))
    }
    // 去负数，取整数，不对外开放
    private abs(str: string|number): string {
        let num = this.fullNum(str)
        return `${ num }`.replace('-', '')
    }
    /**
     * 公式计算，例如：1+2*(5+4)，2的2次方2^2，2的3开方2^(1/3)；如果公式中的某个计算出现错误会直接输出错误。
     * @param value 输入正常公式（标准：*乘，/除，+加，-减，^次方）
     * @returns
     */
    calculator(value: string) {
        // 获取基本数值和计算符号正则字符串
        const number  = '[0-9]+(\\.[0-9]+)?'
        const compute = '(([\\^*/]\\-?)|([+\\-]))'
        // 获取括号中的计算公式段落，优先计算
        const dismantle = (str: string) => {
            // 计算公式正则字符串
            const computed = `(${ number }${ compute })+(${ number })`
            let exp: RegExp
            // 判断是否是带括号的字符串
            if (/\(|\)/.test(str)) {
                // 字符串中是数字还是公式，分别使用不同的判断正则，注意与正常公式一样，括号里的数值也有可能是负数
                const equation = new RegExp(`\\(\\-?${ computed }\\)`)
                const quantitative = new RegExp(`\\((\\-)?${ number }\\)`)
                exp = quantitative.test(str) ? quantitative : equation
            }
            // 以下不是括号中的公式，直接判断并获取字符串
            else if (/[\^*+\-/]/.test(str)) {
                // 字符串中是数字还是公式，分别使用不同的判断正则，注意：这里面只有判断是否为负数
                const minus = new RegExp(`^-${ number }$`)
                const quantitative = new RegExp(`^\\-?${ computed }`)
                exp = minus.test(str) ? minus : quantitative
            }
            // 当不是公式的时候就只要数值，注意：此正则只能判断正数
            else exp = new RegExp(number)
            const text = str.match(exp)
            return text ? text[0] : ''
        }
        return new Promise<string>((resolve, reject) => {
            // 清除字符串空格
            let result: string = value.replace(/\s/g, '')
            // 百分比转数字
            let percent = new RegExp(`${ number }%`)
            result = result.replace(percent, ($1: string) => {
                const num = $1.replace('%', '')
                return this.computeNum(num, 100, '/')
            })
            // 运行函数
            const computedNum = (str: string): boolean => {
                // 获得需要计算的公式
                const text = dismantle(str)
                // 如果为空输出错误信息：错误
                if (!text) {
                    reject('错误')
                    return false
                }
                // 清除()避免影响计算
                let stingNum = text.replace(/^\(|\)$/g, '')
                // 拆分数字和计算符号
                let listExp = new RegExp(`${ compute }?${ number }`, 'g')
                // 拆解公式，把数字和计算符号按顺序放数组
                let list: string[] = []
                let arrList: string[] = stingNum.match(listExp) || []
                arrList.forEach((p, i) => {
                    if (i == 0) list.push(p)
                    else list = list.concat([p.slice(0, 1), p.slice(1, p.length)])
                })
                arrList = list
                // 获取计算符号用来处理计算次数和计算步骤
                let compExp = new RegExp(`^${ compute }$`)
                // 在拆解的数组中获取需要计算的步骤，加减乘除
                let arrComp: string[] = arrList.filter(p => compExp.test(p))
                // 计算先后排序，次方 > 乘除 > 加减
                arrComp.sort((a, b) => {
                    if (['^'].indexOf(a) >= 0 && ['^'].indexOf(b) < 0) return -1
                    else if (['^'].indexOf(b) >= 0 && ['^'].indexOf(a) < 0) return 1
                    else if (['*', '/'].indexOf(a) >= 0 && ['*', '/'].indexOf(b) < 0) return -1
                    else if (['*', '/'].indexOf(b) >= 0 && ['*', '/'].indexOf(a) < 0) return 1
                    return 1
                })
                // 先判断是否需要计算
                if (arrComp.length > 0) {
                    // 计算过程，先乘除后加减，一步步循环计算
                    for(let item of arrComp) {
                        const index = arrList.findIndex(p => item === p)
                        // 判断除法计算中，除数不能为零
                        if (arrList[index] === '/' && arrList[index + 1] == '0') {
                            reject('除数不能为零')
                            return false
                        }
                        // 计算获取到值
                        const num = this.computeNum(arrList[index - 1], arrList[index + 1], arrList[index])
                        arrList.splice(index - 1, 3, num)
                    }
                }
                // 把值替换字符串中当前值的公式字符串，得到新的公式字符串
                result = result.replace(text, arrList[0])
                // 判断公式是否结束，如果还有未计算的结果就再循环一次
                if (isNaN(Number(result))) {
                    // 循环计算公式直至输出结果
                    computedNum(result)
                    return false
                }
                // 计算结束，输出结果
                resolve(result)
                return true
            }
            // 开始计算公式
            computedNum(result)
        })
    }
    /**
     * 取小数点后几位数值的值（默认两位，例如：0.0025589取值为：0.0025，0.25589取值为：0.25， 0.20589取值为：0.2）
     * @param numb 数值/数值字符串
     * @param rate 倍率, 默认为1倍（当前值）
     * @param point 小数点后几位数，默认2位数
     * @returns
     */
    decimalPart(numb: string|number = 0, rate: string|number = 1, point = 2): string {
        let outNum = this.computeNum(numb, rate, '*')
        outNum = this.abs(outNum)
        if (outNum && point) {
            const exp = new RegExp(`^(\\d+[.])(0+)?[1-9][0-9]{0,${ point - 1 }}`)
            const pNum = outNum.match(exp)
            outNum = pNum ? pNum[0] : outNum
            // 清除小数后面的0，判断是否清除小数点
            outNum = outNum.replace(/\.\d+$/g, ($1: string) => {
                return $1.replace(/\.?[0]+$/, '')
            })
        }
        return isNaN(Number(outNum)) ? '' : `${ Number(numb) < 0 ? '-' : '' }${ outNum }`
    }
    /**
     * 千分位
     * @param value 数值/数值字符串
     * @returns
     */
    currencyNo(value: string|number) : string {
        const nums = Number(value)
        if (isNaN(nums) || value == '') return ''
        // 获取整数部分
        // 将数值截取为小数部分和整数部分
        const valueArray = this.fullNum(value).split('.')
        const intPart = valueArray[0]
        // 整数部分处理，增加,
        const intPartFormat = intPart.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
        // 预定义小数部分
        if (valueArray.length === 2) { // 有小数部分
            return intPartFormat + '.' + valueArray[1]
        }
        return intPartFormat
    }
    /**
     * 计算方法，解决乘除浮点数问题
     * @param numOne  数值/数值字符串（必须）
     * @param numTwo  数值/数值字符串（非必须，默认1）
     * @param comp    *乘，/除，-减，+加，^次方（非必须，默认+）
     * @returns
     */
    computeNum(numOne: string|number, numTwo: string|number = 1, comp = '+'): string {
        // 判断numOne和numTwo是否为数字或者数字字符串
        if (isNaN(Number(numOne)) || isNaN(Number(numTwo)) || numOne === '' || numTwo === '') return ''
        // 区分数值的小数点和整数部分
        const arr = this.fullNum(numOne).split('.')
        // 区分倍数的小数点和整数部分
        const rarr = this.fullNum(numTwo).split('.')
        // 获得小数点位数10的倍率
        let beiShu = BigInt(beiShuFn(arr[1]))
        let rBeiShu = BigInt(beiShuFn(rarr[1]))
        // 把数值转成以小数点位数的倍率整数
        const bigintNum = BigInt(arr[0] + (arr[1] || ''))
        const bigintPoint = BigInt(rarr[0] + (rarr[1] || ''))
        // 判断小数点位置的正则
        const exp = new RegExp(`\\d{${ this.digit }}$`)
        // 计算数值
        let compute = BigInt('')
        if (comp === '/') {
            // 除法计算，0不可以做除数
            try {
                compute = bigintNum * this.maxDigit * rBeiShu / bigintPoint / beiShu
            } catch (err: any) {
                return err.message
            }
        } else if (comp === '*') {
            // 乘法计算
            compute = bigintNum * this.maxDigit * bigintPoint / beiShu / rBeiShu
        } else if (comp === '-') {
            // 减法计算
            compute = (bigintNum * this.maxDigit - bigintPoint * this.maxDigit * beiShu / rBeiShu) / beiShu
        } else if (comp === '+') {
            // 加法计算（默认）
            compute = (bigintNum * this.maxDigit + bigintPoint * this.maxDigit * beiShu / rBeiShu) / beiShu
        } else if (comp === '^') {
            // 计算次方值
            const pow: bigint = Number(`${ bigintPoint }`) < 0 ? bigintNum ** -BigInt(`${ rarr[0] }`)
                : bigintNum ** BigInt(rarr[0])
            beiShu = beiShu ** BigInt(rarr[0])
            // 计算根部分
            const sqrt = Math.pow(Number(numOne), Number(`0.${rarr[1] || 0}`))
            const sqrtArr = this.fullNum(sqrt).split('.')
            const qBeiShu = BigInt(beiShuFn(sqrtArr[1]))
            const sqrtNum = BigInt(sqrtArr[0] + (sqrtArr[1] || ''))
            // 次方值和开方根相乘计算
            compute = Number(`${ bigintPoint }`) < 0 ? this.maxDigit * qBeiShu / pow / sqrtNum
                : pow * this.maxDigit * sqrtNum / beiShu / qBeiShu
        }
        // 输出数值，bigint计算值如果小于小数点位数，就按少于多少位先前添加n+1个0转化成字符串
        const until = Number(`${ compute }`) < 0 ? '-' : ''
        let nums = this.abs(`${ compute }`).padStart(this.digit + 1, '0')
        // 正则判断，小数点位置插入
        nums = nums.replace(exp, ($1: string) => `.${ $1 }`)
        // 清除小数后面的0，判断是否清除小数点
        nums = nums.replace(/\.\d+$/g, ($1: string) => {
            return $1.replace(/\.?[0]+$/, '')
        })
        // 输出正负值字符串
        return `${ until + nums }`
    }
    /**
     * 处理科学计算法转数字
     * @param num 数值/数值字符串
     * @returns
     */
    fullNum(num: string|number) : string {
        const numb = Number(num)
        //处理非数字
        if(isNaN(numb) || !numb) return `${ num }`
        //处理不需要转换的数字
        const str = '' + num
        if(!/e/i.test(str)) return `${ num }`
        const list: string[] = `${ num }`.split(/e[-+]/i);
        const nums = `${ list[0] }`.replace('-', '')
        const number = nums.replace('.', '')
        // 基数
        const basisArr: RegExpMatchArray|null = `${ num }`.replace('-', '').match(/^[\d.]+/)
        const basis = basisArr ? basisArr[0].replace(/\./, '') : ''
        const tag = Number(num) > 0 ? '' : '-'
        const text = /e[+]/.test(`${ num }`) ? number.padEnd(Number(list[1]) + 1 || 0, '0') :
            /e[-]/.test(`${ num }`) ? number.padStart(Number(list[1]) + basis.length, '0').replace(/^0/, '0.') :
                ''
        return tag + text
    }
}
